如何打造高性能的 React 应用|原理篇
30 秒速记
- 核心判断:性能优化先建立用户指标与资源、主线程、渲染和网络证据,再定位最长关键路径
- 原理主线:围绕 「善用 shouldComponentUp」、「进阶玩法:PureComponent +」、「函数组件的性能优化:React.memo」 建立输入、状态变化与输出之间的因果关系
- 文章范围:系统讲解了 React 性能优化的核心原理与实践方法,包括 shouldComponentUpdate、PureComponent、React.memo、useMemo 等技术,帮助开发者有效减少不必要的渲染,提升大型 React 应用的性
- 边界与代价:实验室分数不等于真实用户体验,平均值也会掩盖长尾;优化一项指标可能转移成本
- 工程落地:用现场数据确定瓶颈、做最小改动、对照验证并持续监控回归
React 性能优化的关键,是避免没有数据变化却仍然发生的重复渲染或重复计算。 类组件可用 shouldComponentUpdate 精确判断,也可用 PureComponent 对 props 和 state 做浅比较;函数组件则常用 React.memo 控制组件重渲染,用 useMemo 缓存局部计算。浅比较依赖引用稳定,内容未变但引用变了会多渲染,内容变了但引用没变又可能漏渲染。Immutable.js 能把内容变化和新引用关联起来,但存在学习成本,不一定适合所有团队。
这篇文章不要按 API 清单来背。先用上面的 Mind Map 建立全局结构,再通过交互 DEMO 观察正常路径和边界路径如何改变状态;阅读正文时重点核对每一步的输入、负责执行的参与者、产生的中间状态以及最终可观察结果。遇到版本敏感结论,要把“历史实现”“当前行为”和“工程兼容策略”分开说明;遇到性能或架构取舍,则用实际指标、失败现象和验证手段支撑判断。
版本校准: 本文若分析 ReactDOM.render、旧生命周期或栈调和,应把它视为理解架构演进的历史路径。React 19 已移除 ReactDOM.render,当前客户端入口使用 createRoot;并发渲染也必须区分可中断的渲染阶段与同步提交阶段。迁移前对照 React 19 官方升级指南。
- 使用
shouldComponentUpdate规避冗余的更新逻辑 PureComponent + Immutable.jsReact.memo与useMemo
注:这 3 个思路同时也是 React 面试中“性能优化”这一环的核心所在
# 善用 shouldComponentUpdate
shouldComponentUpdate 的调用形式如下:
shouldComponentUpdate(nextProps, nextState)
render方法由于伴随着对虚拟DOM的构建和对比,过程可以说相当耗时。而在React当中,很多时候我们会不经意间就频繁地调用了render。为了避免不必要的render操作带来的性能开销,React 提供了shouldComponentUpdate这个口子。React 组件会根据shouldComponentUpdate的返回值,来决定是否执行该方法之后的生命周期,进而决定是否对组件进行re-render(重渲染)。shouldComponentUpdate的默认值为true,也就是说 “无条件 re-render”。在实际的开发中,我们往往通过手动往shouldComponentUpdate中填充判定逻辑,来实现“有条件的 re-render”。
首先我们来看两个子组件的代码,这里为了尽量简化与数据变更无关的逻辑,ChildA 和 ChildB 都只负责从父组件处读取数据并渲染,它们的编码分别如下所示。
// ChildA.js:
import React from "react";
export default class ChildA extends React.Component {
render() {
console.log("ChildA 的render方法执行了");
return (
<div className="childA">
子组件A的内容:
{this.props.text}
</div>
);
}
}
// ChildB.js:
import React from "react";
export default class ChildB extends React.Component {
render() {
console.log("ChildB 的render方法执行了");
return (
<div className="childB">
子组件B的内容:
{this.props.text}
</div>
);
}
}
// 在共同的父组件 App.js 中,会将 ChildA 和 ChildB 组合起来,并分别向其中注入数据:
import React from "react";
import ChildA from './ChildA'
import ChildB from './ChildB'
class App extends React.Component {
state = {
textA: '我是A的文本',
textB: '我是B的文本'
}
changeA = () => {
this.setState({
textA: 'A的文本被修改了'
})
}
changeB = () => {
this.setState({
textB: 'B的文本被修改了'
})
}
render() {
return (
<div className="App">
<div className="container">
<button onClick={this.changeA}>点击修改A处的文本</button>
<button onClick={this.changeB}>点击修改B处的文本</button>
<ul>
<li>
<ChildA text={this.state.textA}/>
</li>
<li>
<ChildB text={this.state.textB}/>
</li>
</ul>
</div>
</div>
);
}
}
export default App;
App 组件最终渲染到界面上的效果如下图所示,两个子组件在图中分别被不同颜色的标注圈出:

通过点击左右两个按钮,我们可以分别对 ChildA 和 ChildB 中的文案进行修改。
由于初次渲染时,两个组件的 render 函数都必然会被触发,因此控制台在挂载完成后的输出内容如下图所示:

接下来我点击左侧的按钮,尝试对 A 处的文本进行修改。我们可以看到界面上只有 A 处的渲染效果发生了改变,如下图箭头处所示:

但是如果我们打开控制台,会发现输出的内容如下图所示:

这样的输出结果告诉我们,在刚刚的点击动作后,不仅 ChildA 的 re-render 被触发了,ChildB 的 re-render 也被触发了。
在 React 中,
只要父组件发生了更新,那么所有的子组件都会被无条件更新。这就导致了 ChildB 的 props 尽管没有发生任何变化,它本身也没有任何需要被更新的点,却还是会走一遍更新流程。
注: 同样的情况也适用于组件自身的更新:当组件自身调用了 setState 后,那么不管 setState 前后的状态内容是否真正发生了变化,它都会去走一遍更新流程。
而在刚刚这个更新流程中,shouldComponentUpdate 函数没有被手动定义,因此它将返回“true”这个默认值。“true”则意味着对更新流程不作任何制止,也即所谓的“无条件 re-render”。在这种情况下,我们就可以考虑使用 shouldComponentUpdate 来对更新过程进行管控,避免没有意义的 re-render 发生。
现在我们就可以为 ChildB 加装这样一段 shouldComponentUpdate 逻辑:
shouldComponentUpdate(nextProps, nextState) {
// 判断 text 属性在父组件更新前后有没有发生变化,若没有发生变化,则返回 false
if(nextProps.text === this.props.text) {
return false
}
// 只有在 text 属性值确实发生变化时,才允许更新进行下去
return true
}
在这段逻辑中,我们对 ChildB 中的可变数据,也就是 this.props.text 这个属性进行了判断。
这样,当父组件 App 组件发生更新、进而试图触发 ChildB 的更新流程时,shouldComponentUpdate 就会充当一个“守门员”的角色:它会检查新下发的 props.text 是否和之前的值一致,如果一致,那么就没有更新的必要,直接返回“false”将整个 ChildB 的更新生命周期中断掉即可。只有当 props.text 确实发生变化时,它才会“准许” re-render 的发生。
在 shouldComponentUpdate 的加持下,当我们再次点击左侧按钮,试图修改 ChildA 的渲染内容时,控制台的输出就会变成下图这样:

我们看到,控制台中现在只有 ChildA 的 re-render 提示。ChildB “稳如泰山”,成功躲开了一次多余的渲染。
使用 shouldComponentUpdate 来调停不必要的更新,避免无意义的 re-render 发生,这是 React 组件中最基本的性能优化手段,也是最重要的手段。许多看似高级的玩法,都是基于 shouldComponentUpdate 衍生出来的。我们接下来要讲的 PureComponent,就是这类玩法中的典型。
# 进阶玩法:PureComponent + Immutable.js
PureComponent:提前帮你安排好更新判定逻辑
shouldComponentUpdate虽然一定程度上帮我们解决了性能方面的问题,但每次避免 re-render,都要手动实现一次shouldComponentUpdate,未免太累了。作为一个不喜欢重复劳动的前端开发者来说,在写了不计其数个shouldComponentUpdate逻辑之后,难免会怀疑人生
React 15.3 很明显听到了开发者的声音,它新增了一个叫 PureComponent 的类,恰到好处地解决了“程序员写 shouldComponentUpdate 写出腱鞘炎”这个问题。
PureComponent 与 Component 的区别点,就在于它内置了对shouldComponentUpdate的实现:PureComponent将会在shouldComponentUpdate中对组件更新前后的props 和 state 进行浅比较,并根据浅比较的结果,决定是否需要继续更新流程。
import React from "react";
export default class ChildB extends React.PureComponent {
render() {
console.log("ChildB 的render方法执行了");
return (
<div className="childB">
子组件B的内容:
{this.props.text}
</div>
);
}
}
此时再去修改 ChildA 中的文本,我们会发现 ChildB 同样不受影响
